<?php
/**
 * RBAC (Role Based Access Control) system.
 *
 * @copyright 2009-2019 Vanilla Forums Inc.
 * @license GPL-2.0-only
 * @package Dashboard
 * @since 2.0
 */

use Vanilla\Permissions;

/**
 * Handles /role endpoint.
 */
class RoleController extends DashboardController
{
    /** @var bool Should categories be hidden when editing a role? */
    private $hideCategoryPermissions;

    /** @var Gdn_Form */
    public $Form;

    /** @var array Models to automatically instantiate. */
    public $Uses = ["Database", "Form", "RoleModel"];

    /** @var RoleModel */
    public $RoleModel;

    /**
     * RoleController constructor.
     */
    public function __construct()
    {
        parent::__construct();
        $this->hideCategoryPermissions = c("Vanilla.HideRoleCategoryPermissions", false);
    }

    /**
     * Set menu path. Automatically run on every use.
     *
     * @since 2.0.0
     * @access public
     */
    public function initialize()
    {
        parent::initialize();
        Gdn_Theme::section("Settings");
        if ($this->Menu) {
            $this->Menu->highlightRoute("/dashboard/settings");
        }
    }

    /**
     * Create new role.
     *
     * @since 2.0.0
     * @access public
     */
    public function add()
    {
        if (!$this->_permission()) {
            return;
        }

        $this->title(t("Add Role"));

        // Use the edit form with no roleid specified.
        $this->View = "Edit";
        $this->edit();
    }

    /**
     * Remove a role.
     *
     * @since 2.0.0
     * @access public
     */
    public function delete($roleID = false)
    {
        if (!$this->_permission($roleID)) {
            return;
        }

        $this->title(t("Delete Role"));
        $this->setHighlightRoute("dashboard/role");

        $role = $this->RoleModel->getByRoleID($roleID);
        if ($role->Deletable == "0") {
            $this->Form->addError("You cannot delete this role.");
        }

        // Make sure the form knows which item we are deleting.
        $this->Form->addHidden("RoleID", $roleID);

        // Figure out how many users will be affected by this deletion
        $this->AffectedUsers = $this->RoleModel->getUserCount($roleID);

        // Figure out how many users will be orphaned by this deletion
        $this->OrphanedUsers = $this->RoleModel->getUserCount($roleID, true);

        // Get a list of roles other than this one that can act as a replacement
        $this->ReplacementRoles = $this->RoleModel->getByNotRoleID($roleID);

        if ($this->Form->authenticatedPostBack()) {
            // Make sure that a replacement role has been selected if there were going to be orphaned users
            if ($this->OrphanedUsers > 0) {
                $validation = new Gdn_Validation();
                $validation->applyRule(
                    "ReplacementRoleID",
                    "Required",
                    "You must choose a replacement role for orphaned users."
                );
                $validation->validate($this->Form->formValues());
                $this->Form->setValidationResults($validation->results());
            }
            if ($this->Form->errorCount() == 0) {
                // Go ahead and delete the Role
                $this->RoleModel->deleteAndReplace($roleID, $this->Form->getValue("ReplacementRoleID"));
                $this->setRedirectTo("dashboard/role");
                $this->informMessage(t("Deleting role..."));
            }
        }
        $this->render();
    }

    /**
     * Edit a role.
     *
     * @param int|bool $RoleID
     * @since 2.0.0
     * @access public
     */
    public function edit($roleID = false)
    {
        if (!$this->_permission($roleID)) {
            return;
        }

        if ($this->title() == "") {
            $this->title(t("Edit Role"));
        }

        $this->setHighlightRoute("dashboard/role");
        $permissionModel = Gdn::permissionModel();
        $roleSync = !empty(c("Garden.SSO." . UserModel::OPT_ROLE_SYNC));
        $this->Role = $this->RoleModel->getByRoleID($roleID);
        if ($this->Role) {
            if (in_array($this->Role->Type, [RoleModel::TYPE_MODERATOR, RoleModel::TYPE_ADMINISTRATOR])) {
                $this->Role->Type = "";
            }
            $this->Role->EnableType =
                !stringIsNullOrEmpty($this->Role->Type) || !stringIsNullOrEmpty($this->Role->Domains);
            if (
                in_array($this->Role->RoleID, RoleModel::DEFAULT_ROLE_IDS) &&
                stringIsNullOrEmpty($this->Role->Domains)
            ) {
                $this->Role->EnableType = false;
            }
        }
        $this->addJsFile("jquery.gardencheckboxgrid.js");
        $this->addJsFile("manage-roles.js", "vanilla");
        // Set the model on the form.
        $this->Form->setModel($this->RoleModel);

        // Make sure the form knows which item we are editing.
        $this->Form->addHidden("RoleID", $roleID);

        $limitToSuffix = !$this->Role || $this->Role->CanSession == "1" ? "" : "View";

        // If seeing the form for the first time...
        if ($this->Form->authenticatedPostBack() === false) {
            // Get the role data for the requested $RoleID and put it into the form.
            $permissions = $permissionModel->getPermissionsEdit(
                $roleID ? $roleID : 0,
                $limitToSuffix,
                $this->hideCategoryPermissions === false
            );

            // Remove permissions the user doesn't have access to.
            if (!Gdn::session()->checkPermission("Garden.Settings.Manage")) {
                foreach ($this->RoleModel->RankPermissions as $permission) {
                    if (Gdn::session()->checkPermission($permission)) {
                        continue;
                    }

                    [$px, $sx] = explode(".", $permission, 2);
                    unset($permissions["_" . $px][$sx]);
                }
            }

            $this->setData("PermissionData", $permissions, true);

            $this->Form->setData($this->Role);
        } else {
            $this->removeRankPermissions();
            if (!$roleSync) {
                $this->Form->removeFormValue("Sync");
            } else {
                $this->Form->setFormValue("Sync", (string) $this->Form->getFormValue("Sync"));
            }

            // Make sure the role's checkbox has a false value so that the role model can handle a sparse update of
            // column from other places.
            if (!$this->Form->getFormValue("PersonalInfo")) {
                $this->Form->setFormValue("PersonalInfo", false);
            }

            if ($this->hideCategoryPermissions) {
                $this->Form->setFormValue("IgnoreCategoryPermissions", true);
            }

            // If the form has been posted back...
            // 2. Save the data (validation occurs within):
            if ($roleID = $this->Form->save()) {
                if ($this->deliveryType() === DELIVERY_TYPE_DATA) {
                    $this->index($roleID);
                    return;
                }

                $permissionData = $permissionModel->getPermissionsEdit(
                    $roleID,
                    $limitToSuffix,
                    $this->hideCategoryPermissions === false
                );

                $this->informMessage(t("Your changes have been saved."));
                $this->setRedirectTo("dashboard/role");
            } else {
                $overrides = $this->Form->getFormValue("Permission");
                $permissionData = $permissionModel->getPermissionsEdit(
                    $roleID,
                    $limitToSuffix,
                    $this->hideCategoryPermissions === false,
                    $overrides
                );
            }
            // Reload the permission data.
            $this->setData("PermissionData", $permissionData, true);
        }

        $this->setData("_Types", $this->RoleModel->getDefaultRoleTypes());
        $this->setData("_roleSyncVisible", $roleSync);

        $this->render();
    }

    /**
     * Show list of roles.
     *
     * @since 2.0.0
     * @access public
     */
    public function index($roleID = null)
    {
        $this->_permission();

        $this->setHighlightRoute("dashboard/role");
        $this->addJsFile("jquery.tablednd.js");
        $this->title(t("Roles & Permissions"));

        if (!$roleID) {
            $roles = $this->RoleModel->getWithRankPermissions()->resultArray();

            // Check to see which roles can be modified.
            foreach ($roles as &$row) {
                $canModify = true;

                if (!Gdn::session()->checkPermission("Garden.Settings.Manage")) {
                    foreach ($this->RoleModel->RankPermissions as $permission) {
                        if ($row[$permission] && !Gdn::session()->checkPermission($permission)) {
                            $canModify = false;
                            break;
                        }
                    }
                }
                $row["CanModify"] = $canModify;
            }
            $this->setData("Roles", $roles);
        } elseif ($this->deliveryType() === DELIVERY_TYPE_DATA) {
            // This is an API request. Get the role in a nicer format.
            $role = $this->RoleModel->getID($roleID, DATASET_TYPE_ARRAY);

            // Get the global permissions.
            $permissions = Gdn::permissionModel()->getGlobalPermissions($roleID);
            unset($permissions["PermissionID"]);

            // Get the per-category permissions.
            $permissions["Category"] = $this->RoleModel->getCategoryPermissions($roleID);

            $role["Permissions"] = $permissions;

            $this->setData("Role", $role);
            saveToConfig("Api.Clean", false, false);
        } else {
            $Role = $this->RoleModel->getID($roleID);
            $this->setData("Roles", [$Role]);
        }

        // Grab the total users for each role.
        if (is_array($this->data("Roles"))) {
            $pastThreshold = Gdn::userModel()->pastUserMegaThreshold();
            $thresholdTypeExceptions = RoleModel::getDefaultTypes();
            unset($thresholdTypeExceptions[RoleModel::TYPE_MEMBER]);

            $roles = $this->data("Roles");
            foreach ($roles as &$role) {
                if ($pastThreshold && !in_array($role["Type"], $thresholdTypeExceptions)) {
                    $countUsers = t("View");
                } else {
                    $countUsers = $this->RoleModel->getUserCount($role["RoleID"]);
                }

                $role["CountUsers"] = $countUsers;
            }
            $this->setData("Roles", $roles);
        }

        $this->render();
    }

    /**
     * Advanced settings for roles & permissions.
     */
    public function advanced()
    {
        $this->permission("Garden.Settings.Manage");

        $aliases = Permissions::getRankedPermissionAliases(true);
        foreach ($aliases as $key => &$alias) {
            $alias = t("permissions." . $alias);
        }

        $cf = new ConfigurationModule($this);
        $cf->initialize([
            "Garden.PrivateCommunity" => [
                "LabelCode" => "Enable Private Communities",
                "Description" => t("Once enabled, only members will see inside your community."),
                "Control" => "toggle",
            ],
            "Garden.api.ssoIDPermission" => [
                "LabelCode" => "API SSO Expand Permission",
                "Description" => t("Garden.api.ssoIDPermission.description"),
                "Control" => "DropDown",
                "Items" => $aliases,
                "Default" => Permissions::RANK_COMMUNITY_MANAGER,
            ],
        ]);
        $this->setData("Title", t("Advanced Settings"));
        $cf->renderAll();
    }

    /**
     * Do permission check.
     *
     * @param int $roleID
     * @since 2.0.0
     * @access protected
     */
    protected function _permission($roleID = null)
    {
        $this->permission(["Garden.Settings.Manage", "Garden.Roles.Manage"], false);

        if ($roleID && !checkPermission("Garden.Settings.Manage")) {
            // Make sure the user can assign this role.
            $assignable = $this->RoleModel->getAssignable();
            if (!isset($assignable[$roleID])) {
                throw permissionException("@" . t("You don't have permission to modify this role."));
            }
        }
        return true;
    }

    /**
     *
     */
    protected function removeRankPermissions()
    {
        if (Gdn::session()->checkPermission("Garden.Settings.Manage")) {
            return;
        }

        // Remove ranking permissions.
        $permissions = $this->Form->getFormValue("Permission");
        foreach ($this->RoleModel->RankPermissions as $permission) {
            if (!Gdn::session()->checkPermission($permission) && in_array($permission, $permissions)) {
                $index = array_search($permission, $permissions);
                unset($permissions[$index]);
            }
        }
        $this->Form->setFormValue("Permission", $permissions);
    }
}
